/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package main;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import weka.core.Instance;
import weka.core.Instances;
import weka.core.converters.ArffLoader;

/**
 *
 * @author Reinhard
 */
public class MyIBk implements MyClassifier {
  public Instances inst;
  public ArrayList<Instances> train, test;//Untuk cross validation
  public ArrayList<DistPair> dist; // jarak,first untuk id instances,
  public ArrayList<Boolean> isDiscretized;//true untuk yang telah diskrit
  public ArrayList<Double> maxValue;//maximal untuk rentang
  public int knn;
  private double accuracy;
  private int correct = 0;

  /**
   * Comparator for sorting pair
   */
  private static class compareDist implements Comparator<DistPair> {
    @Override
    public int compare(DistPair o1, DistPair o2) {
      return (o1.dist < o2.dist ? -1 : (o1.dist == o2.dist ? 0 : 1));
    }
  }

  /**
   * Check if a given string is a double or not
   * @param str
   * @return true if str is double type number
   */
  public static boolean isNumeric(String str) {
    try {
      double d = Double.parseDouble(str);
    } catch (NumberFormatException nfe) {
      return false;
    }
    return true;
  }

  /**
   * Default constructor
   */
  public MyIBk(){
      
  }
  
  /**
   * MyIBk constructor
   * @param inst the training set
   * @param knn number of neighbours
   */
  public MyIBk(Instances inst, int knn) {
    this.inst = inst;
    this.knn = knn;
    isDiscretized = new ArrayList<Boolean>();
    maxValue = new ArrayList<Double>();

    for (int i = 0; i < inst.numAttributes(); ++i) {
      double maks = Double.MIN_VALUE;
      try {
        if (inst.instance(0).attribute(i).isNumeric()) {
          for (int j = 0; j < inst.numInstances(); ++j) {
            maks = Math.max(maks, inst.instance(j).value(i));
          }
        }
      } catch (NumberFormatException ex) {
//        ex.printStackTrace();
      }
      maxValue.add(maks);
    }

    for (int i = 0; i < inst.numAttributes(); ++i) {
      boolean discretized = false;
      String atributkei = "";
      try {
        if (inst.instance(0).attribute(i).isNumeric()) {
          atributkei = Double.toString(inst.instance(0).value(i));
        }
      } catch (NumberFormatException ex) {
        atributkei = inst.instance(0).stringValue(i);
      }

      if (!isNumeric(atributkei)) {
        discretized = true;
      } else {
        discretized = false;
      }
      if (discretized) {
        isDiscretized.add(true);
      } else {
        isDiscretized.add(false);
      }
    }
  }

  /**
   * Set the number of neighbors for IBk
   * @param knn the number of neighbors wanted
   */
  public void setKNN(int knn) {
    this.knn = knn;
  }

  /**
   * Set the training set
   * @param inst Instances to be used to train the classifier.
   */
  public void setInstances(Instances inst) {
    this.inst = inst;
  }

  /**
   * Compute the distance between two Instance
   * Defined to be the sum of absolute difference between each attribute, divided
   * by maximum value for that attribute and have weight of 1/19 for each attribute,
   * bearing in mind that the total number of attribute is 19.
   * @param a first Instance
   * @param b second Instance
   * @return distance between two Instance
   */
  public double computeDist(Instance a, Instance b) {
    double ret = 0.0;
    for (int i = 0; i < a.numAttributes() - 1; ++i) {
      double tot = 0.0;
      if (isDiscretized.get(i)) {
        if (a.stringValue(i).equals(b.stringValue(i))) {
          tot += 0.0;
        } else {
          tot += 1.0;
        }
      } else {
        tot += Math.abs(a.value(i) - b.value(i));
        tot /= maxValue.get(i);
      }
      tot /= (inst.numAttributes() - 1);
      ret += tot;
    }
    return ret;
  }

  /**
   * Learn with training set, data test menggunakan test
   */
  public void doClassification(Instances test) {
    correct = 0;
    for (int i = 0; i < test.numInstances(); ++i) {
      HashMap<String, Integer> hm = new HashMap<String, Integer>();
      dist = new ArrayList<DistPair>();
      for (int j = 0; j < inst.numInstances(); ++j) {
        double d = computeDist(test.instance(i), inst.instance(j));
        if (d > 1e-9) {
          dist.add(new DistPair(j, d));
        }
      }

//      for (int j = 0; j < dist.size(); ++j) {
//        System.out.println(j + " " + dist.get(j).id + " " + dist.get(j).dist);
//      }

      Collections.sort(dist, new compareDist());

      for (int j = 0; j < knn; ++j) {
        if (hm.containsKey(inst.instance(dist.get(j).id).stringValue(inst.numAttributes() - 1))) {
          int oldval = (Integer) hm.get(inst.instance(dist.get(j).id).stringValue(inst.numAttributes() - 1));
          hm.put(inst.instance(dist.get(j).id).stringValue(inst.numAttributes() - 1), oldval + 1);
        } else {
          hm.put(inst.instance(dist.get(j).id).stringValue(inst.numAttributes() - 1), 1);
        }
      }

      String res = "";
      Integer maks = Integer.MIN_VALUE;
      for (Map.Entry<String, Integer> entry : hm.entrySet()) {
        String key = entry.getKey();
        Integer value = entry.getValue();
        if (value >= maks) {
          res = key;
          maks = value;
        }
      }
//      System.out.println(res + " " + test.instance(i).stringValue(test.numAttributes() - 1));
      if (res.equals(test.instance(i).stringValue(test.numAttributes() - 1))) {
        ++correct;
      }
    }
//    System.out.println(correct + "/" + test.numInstances());
    accuracy = (double) correct / test.numInstances();
  }

  /**
   * Do cross validation
   * @param folds number of folds. 1 < folds <= numInstances.
   */
  public void doCrossValidation(int folds) {
    Instances awal = new Instances(inst);
    ArrayList<Instances> ai = new ArrayList<Instances>();
    int bagi = awal.numInstances() / folds;
    int sisa = awal.numInstances() % folds;
//    System.out.println("bagi = " + bagi + ",sisa = " + sisa);
    for (int i = 0; i < folds; ++i) {
      Instances add;
//      System.out.println(i + " " + i * bagi);
      if (i == folds - 1 && sisa != 0) {
        add = new Instances(awal, i * bagi, sisa);
      } else {
//        System.out.println((i*folds) + " " + folds);
        add = new Instances(awal, i * bagi, bagi);
      }
      ai.add(add);
    }

    double final_accuracy = 0.0;
    for (int i = 0; i < ai.size(); ++i) {
      Instances newtest = ai.get(i);
      Instances newtrain = ai.get(i); // dummy
      boolean pertamax = true;
      for (int j = 0; j < ai.size(); ++j) {
        if (i != j) {
          if (pertamax) {
            newtrain = new Instances(ai.get(j));
            pertamax = false;
            continue;
          }
          for (int k = 0; k < ai.get(j).numInstances(); ++k) {
            newtrain.add(ai.get(j).instance(k));
          }
        }
      }
      setInstances(newtrain);
      doClassification(newtest);
//      System.out.println(accuracy + " ");
      final_accuracy += accuracy;
    }

    final_accuracy /= folds;
    accuracy = final_accuracy;
//    for(int i = 0; i < folds; ++i) {
////      System.out.println("numinstance = " + instnew.numInstances());
////      System.out.println("i = " + i);
//      Instances newtest = new Instances(awal, i, folds);
//      Instances newtrain = new Instances(awal, 0, awal.numInstances());
//      setInstances(newtrain);
//      doClassification(newtest);
//      tot_accuracy += accuracy;
//      System.out.println(accuracy + " ");
//    }
  }

  /**
   * Do perecentage split
   * @param percent percentage of data training to be used for learning. 100-percent % will be used for test.
   */
  public void doPercentageSplit(double percent) {
    inst.randomize(new Random(1));
    int trainSize = (int) Math.round(inst.numInstances() * percent / 100);
    int testSize = inst.numInstances() - trainSize;
    Instances newtrain = new Instances(inst, 0, trainSize);
    Instances newtest = new Instances(inst, trainSize, testSize);
//      System.out.println("newtrain : " + newtrain);
//      System.out.println("newtest : " + newtest);
    setInstances(newtrain);
    doClassification(newtest);
  }

  /**
   * Get classification accuracy
   * @return accuracy of MyIBk classifier
   */
  public double getAccuracy() {
    return accuracy*100;
  }
  
  /**
   * Get correct number of classification
   * Should be called after doing one of the classification methods.
   * @return number of correct classification
   */
  public int getCorrectClassification() {
    return correct;
  }

  /**
   * Get classification model
   * @return model of classifier. N/A for IBk
   */
  public String getModel() {
    return "N/A";
  }

  public static void main(String[] args) throws IOException {
//    String filename = "D:\\Kuliah\\Semester 6\\IF3054 - Intelegensia Buatan\\WEKA\\segmentation_data.arff";
    String filename = "src/java/data_set/image_segmentation_data.arff";
    ArffLoader loader = new ArffLoader();
    Instances inst;
    loader.setSource(new File(filename));
    inst = loader.getDataSet();
    inst.setClassIndex(inst.numAttributes() - 1);
    Instances ori = new Instances(inst);
    ori.setClassIndex(ori.numAttributes() - 1);
    
    MyIBk mi = new MyIBk(inst, 1);
    System.out.println("Train set");
    mi.doClassification(inst);
    System.out.println(mi.getAccuracy());

    System.out.println("Cross Validation");
    mi = new MyIBk(ori, 10);
    mi.setInstances(ori);
    mi.doCrossValidation(15); // dah lumayan
    System.out.println(mi.getAccuracy());

    System.out.println("Precentage Split");
    mi = new MyIBk(inst, 1);
    mi.setInstances(inst);
    mi.doPercentageSplit(66); // dah lumayan
    System.out.println(mi.getAccuracy());
  }
  
}
